<?php
/**
* Basic ORM class
*
* @author Matt Pike
* @package Mhtml
*/
class Model{
	/** @var string Name of the table in the db schema relating to child class */
	public $_tbl	= '';
	/** @var string Name of the primary key field in the table */
	public $_tbl_key	= '';
	/** @var string Error message */
	public $_error 	= '';
	/**
	 * @var mosDatabase
	 */
	public $_db 		= null;
	/** @var array schema */
	public $_schema		= null;
	/** @var array dependencies/relation vars */
	protected $_foreignObjects = null;
	protected $_permissions = 2; // by default have read/write access

	/**
	 * Maintain a reference to all loaded objects so they can be retrieved again later in the code
	 *
	 * @var array
	 */
	public static $_cache = array();

	/**
	*	Object constructor to set table and key field
	*
	*	Can be overloaded/supplemented by the child class
	*/
	function __construct($loadId=null,$withPermissions=false) {
		$this->_db 	= MhtmlCore::getDatabase();
		if($loadId && is_numeric($loadId)){
			$this->load(intval($loadId),$withPermissions);
			MhtmlDebug::getInstance()->trace("Loading $this->_tbl($loadId) = ".($this->id ? "Success" : "Failed"));
		}
	}

	/**
	 * Load a db model off a local foreign key
	 *
	 * @param string $modelName
	 * @param string $foreignKey
	 * @param string $fkPostfix
	 */
	public function loadDependency($modelName,$foreignKey,$newVar){
		if(class_exists($modelName)){
			if(!isset($this->$newVar)){
				$this->$newVar = new $modelName;
				$success = $this->$newVar->load($this->$foreignKey);
				if($success){
					$this->_foreignObjects[$foreignKey] = $newVar;
					return $success;
				}
			}else{
				MhtmlDebug::trace("Retrieving cached dependancy {$modelName}->{$newVar}");
				return $this->$newVar;
			}
		}else{
			trigger_error("Data model '$modelName' not found.",E_USER_ERROR);
		}
	}

	public function reloadDependencies(){
		foreach($this->_foreignObjects as $fk=>$store){
			if(is_subclass_of($this->$store,"MhtmlModel")){
				$this->$store->load($this->$fk);
			}
		}
	}

	/**
	 * Load db models from a foreign table based on their FK
	 *
	 * @param string $table
	 * @param string $fk
	 * @param string $newVar
	 * @param string $conditions
	 */
	public function loadRelations($modelName,$fk,$newVar,$conditions=""){
		if(isset($this->$newVar)){
			MhtmlDebug::trace("Retrieving cached relations {$modelName}->{$newVar}");
			return $this->$newVar;
		}elseif(class_exists($modelName)){
			$mdl = new $modelName;
			$q = "SELECT id FROM $mdl->_tbl WHERE $fk='$this->id'".(($conditions) ? " AND $conditions" : '')." ORDER BY id ASC";
			$this->_db->setQuery($q);
			$res = $this->_db->loadObjectList();
			$this->$newVar = array();
			foreach($res as $row){
				$mdl = new $modelName;
				if($mdl->load($row->id)){
					$this->_foreignObjects[$fk] = $newVar;
					array_push($this->$newVar,$mdl);
				}
			}
		}else{
			throw new GeneralException("Data model '$modelName' not found.");
		}
	}

	/**
	 * Returns an array of public properties
	 * @return array
	 */
	protected function getPublicProperties() {
		static $cache = null;
		if (is_null( $cache )) {
			$cache = array();
			foreach (get_class_vars( get_class( $this ) ) as $key=>$val) {
				if (substr( $key, 0, 1 ) != '_') {
					$cache[] = $key;
				}
			}
		}
		return $cache;
	}

	public static function getName($id,$tbl){
		$db = MhtmlCore::getDatabase();
		$db->setQuery("SELECT name FROM $tbl WHERE id=".(int) $id);
		return $db->loadResult();
	}

	/**
	 * Sets the named schema array
	 * @private
	 * @since 1.0.6
	 *
	function _setSchema( $array=null ) {
		if (is_array( $array )) {
			$this->_schema = $array;
		} else {
			$tableFields = $this->_db->getTableFields( array( $this->_tbl ) );
			$this->_schema = $tableFields[$this->_tbl];
		}
	}*/

	/**
	 * Gets the schema array
	 * @private
	 * @since 1.0.6
	 *
	function _getSchema() {
		if ($this->_schema == null) {
			$this->_setSchema();
		}
		return $this->_schema;
	}*/

	/**
	 * Returns the [database] type of the field
	 * @param string The name of the field
	 * @return string The field type
	 * @private
	 * @since 1.0.6
	 *
	function _getFieldType( $name ) {
		$schema = $this->_getSchema();

		if (isset( $schema[$name] )) {
			$result = $schema[$name];
		} else {
			$result = 'text';
		}
		return $result;
	}*/

	/**
	 * Filters public properties
	 * @access protected
	 * @param array List of fields to ignore
	 *
	function filter( $ignoreList=null ) {
		$ignore = is_array( $ignoreList );

		$iFilter = new InputFilter();
		foreach ($this->getPublicProperties() as $k) {
			if ($ignore && in_array( $k, $ignoreList ) ) {
				continue;
			}
			$this->$k = $iFilter->process( $this->$k );
		}
	}*/

	/**
	 *	@return string Returns the error message
	 */
	function getError() {
		return $this->_error;
	}

	/**
	* Gets the value of the class variable
	* @param string The name of the class variable
	* @return mixed The value of the class var (or null if no var of that name exists)
	*
	function get( $_property ) {
		if(isset( $this->$_property )) {
			return $this->$_property;
		} else {
			return null;
		}
	}*/

	public static function getCount($tableName,$where=""){
		$db = MhtmlCore::getDatabase();
		$q = "SELECT count(*) as count FROM `".$tableName."`";
		if($where) $q .= " WHERE ".$where;
		$db->setQuery($q);
		return $db->loadResult();
	}

	/**
	 * Return an associative array, use getOptionList() for select boxes
	 */
	public static function getAssocArray($tableName,$innerCol="name",$valCol="id",$where="",$orderBy="innerText ASC"){
		$db = MhtmlCore::getDatabase();
		$q = "SELECT $valCol as valueIndex, $innerCol as innerText FROM `".$tableName."`";
		if($where) $q .= " WHERE ".$where;
		if($orderBy) $q .= " ORDER BY ".$orderBy;
		$db->setQuery($q);
		$options = $db->loadObjectList();
		$returnArr = array();
		if(is_array($options)){
			foreach($options as $option){
				$returnArr[$option->innerText] = $option->valueIndex;
			}
		}
		return $returnArr;
	}

	/**
	 * Return html options for a select box
	 */
	public function getOptionList($tableName,$defaultSelected="",$innerCol="name",$valCol="id",$where="",$startingText="- Please Select -",$orderBy="innerText ASC"){
		$html = "";
		$optList = self::getAssocArray($tableName,$innerCol,$valCol,$where,$orderBy);
		if($startingText) $html .= "<option value=''>$startingText</option>";
		foreach($optList as $k=>$v){
			$selected = ($v==$defaultSelected) ? " selected='selected'" : "";

			$html .= "<option$selected value=\"".htmlentities($v)."\">$k</option>";
		}
		return $html;
	}

	/**
	 * Returns object properties in an assoc array
	 *
	 * @return array
	 */
	public function toArray($ignore=""){
		$tmp = get_object_vars($this);
		$ret = array();
		$ignore = explode(" ",$ignore);
		foreach($tmp as $k=>$v){
			if($k[0]!="_" && !in_array($k,$ignore)){
				$ret[$k] = $v;
			}
		}
		return $ret;
	}

	/**
	 * Returns "name" field based on id
	 *
	 * @param int
	 * @return string
	 */
	public function getRecordName($id){
		$this->_db->setQuery("SELECT name FROM ".$this->_db->getEscaped($this->_tbl)." WHERE ".$this->_db->getEscaped($this->_tbl_key)."='".intval($id)."'");
		return $this->_db->loadResult();
	}

	/**
	 * Get a TOTAL count of items for table
	 *
	 * @param string $where
	 * @return int
	 */
	function getTableCount($where=""){
		$q = "SELECT count(*) FROM ".$this->_tbl;
		if($where) $q .= " WHERE ".$where;
		$this->_db->setQuery($q);
		return $this->_db->loadResult();
	}

	/**
	 * #*# Returns an array of DB objects
	 *
	 * @param string $where - sql safe expression
	 */
	function getObjList($where=""){
		$q = "SELECT * FROM `".$this->_tbl."`";
		if($q) $q.= " WHERE $where";
		$this->_db->setQuery($q);
		return $this->_db->loadObjectList();
	}

	/**
	* Set the value of the class variable
	* @param string The name of the class variable
	* @param mixed The value to assign to the variable
	*/
	function set( $_property, $_value ) {
		$this->$_property = $_value;
	}

	/**
	 * Resets public properties
	 * @param mixed The value to set all properties to, default is null
	 */
	function reset( $value=null ) {
		$keys = $this->getPublicProperties();
		foreach ($keys as $k) {
			$this->$k = $value;
		}
	}
	/**
	*	binds a named array/hash to this object
	*
	*	can be overloaded/supplemented by the child class
	*	@param array $hash named array
	*   @param string $ignore space seperated list
	*	@return null|string	null is operation was satisfactory, otherwise returns an error
	*/
	function bind( $array, $ignore='' ) {
		if (!is_array( $array )) {
			$this->_error = strtolower(get_class( $this ))."::bind failed.";
			return false;
		} else {
			return mosBindArrayToObject( $array, $this, $ignore );
		}
	}

	/**
	* binds an array/hash to this object
	* @param int $oid optional argument, if not specifed then the value of current key is used
	* @param boolean $withPermissions Calls getPermission() on model
	* @param boolean $checkCache if same model and record has been loaded previously, return that
	* @return any result from the database operation
	* @todo replace * with actual fields
	*/
	function load( $oid=null,$withPermissions=false, $checkCache=true ){
		if($withPermissions){
			if(method_exists($this,"getPermission")){
				$this->_permissions = $this->getPermission($oid);
				if(!$this->_permissions) return false;
			}else{
				throw new GeneralException("Object loaded with permissions but no permissions method present!");
			}
		}

		$k = $this->_tbl_key;

		if ($oid !== null) {
			$this->$k = $oid;
		}

		$oid = $this->$k;

		if ($oid === null) {
			return false;
		}
		//Note: Prior to PHP 4.2.0, Uninitialized class variables will not be reported by get_class_vars().
		$class_vars = get_class_vars(get_class($this));
		foreach ($class_vars as $name => $value) {
			if (($name != $k) and ($name != "_db") and ($name != "_tbl") and ($name != "_tbl_key")) {
				$this->$name = $value; // @todo make sure static properties cant be accessed
			}
		}

		$this->reset();

		$query = "SELECT *"
		. "\n FROM $this->_tbl"
		. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $oid )
		;
		$this->_db->setQuery( $query );
		$result = $this->_db->loadObject( $this );
		// Cache result
		self::$_cache[$this->_tbl][$this->_tbl_key][$oid] = $this;

		return $result;
	}

	/**
	*	generic check method
	*
	*	can be overloaded/supplemented by the child class
	*	@return boolean True if the object is ok
	*/
	function check() {
		return true;
	}

	/**
	* Inserts a new row if id is zero or updates an existing row in the database table
	*
	* @param boolean If false, null object variables are not updated
	* @return null|string null if successful otherwise returns and error message
	*/
	function store($updateNulls=false) {
		$k = $this->_tbl_key;
		$stOrUpd = is_null($this->$k) ? "Adding" : "Updating";
		if($this->_permissions > 1){
			if(count($this->_foreignObjects)){
				foreach($this->_foreignObjects as $fk => $dep){
					if(isset($this->$dep) && is_array($this->$dep) && count($this->$dep)){
						foreach($this->$dep as $rec){
							if(is_subclass_of($rec,"MhtmlModel")){
								$rec->store($updateNulls);
							}else{
								MhtmlDebug::log("Object not subclass of MhtmlModel",$rec);
							}
						}
					}elseif(isset($this->$dep) && !is_array($this->$dep) && is_subclass_of($this->$dep,"MhtmlModel")){
						if($this->$fk==$this->$dep->id){
							$this->$dep->store($updateNulls);
						}else{
							MhtmlDebug::log("The foreign key value has changed since loading of the dependancy \$this->$dep, unable to save.");
						}
					}else{
						MhtmlDebug::log("Unknown foreign object",$this->$dep);
					}
				}
			}

			if ($this->$k) {
				$ret = $this->_db->updateObject( $this->_tbl, $this, $this->_tbl_key, $updateNulls );
			} else {
				$ret = $this->_db->insertObject( $this->_tbl, $this, $this->_tbl_key );
			}

			if( !$ret ) {
				$this->_error = strtolower(get_class( $this ))."::store failed - " . $this->_db->getErrorMsg();
				MhtmlDebug::getInstance()->trace("$stOrUpd $this->_tbl($this->id) = Failed (".$this->_db->getErrorMsg().")");
				return false;
			} else {
				MhtmlDebug::getInstance()->trace("$stOrUpd $this->_tbl($this->id) = Success");
				return true;
			}
		}else{
			$my = MhtmlCore::getUser();
			$this->_error = get_class( $this )."::$stOrUpd failed - Invalid write permissions for user id ".$my->id;
			MhtmlDebug::log("$stOrUpd $this->_tbl($this->id) = Failed (Invalid write permissions)");
			return false;
		}
	}
	/**
	* @param string $where This is expected to be a valid (and safe!) SQL expression
	*/
	function move( $dirn, $where='' ) {
		$k = $this->_tbl_key;

		$sql = "SELECT $this->_tbl_key, ordering FROM $this->_tbl";

		if ($dirn < 0) {
			$sql .= "\n WHERE ordering < " . (int) $this->ordering;
			$sql .= ($where ? "\n	AND $where" : '');
			$sql .= "\n ORDER BY ordering DESC";
			$sql .= "\n LIMIT 1";
		} else if ($dirn > 0) {
			$sql .= "\n WHERE ordering > " . (int) $this->ordering;
			$sql .= ($where ? "\n	AND $where" : '');
			$sql .= "\n ORDER BY ordering";
			$sql .= "\n LIMIT 1";
		} else {
			$sql .= "\nWHERE ordering = " . (int) $this->ordering;
			$sql .= ($where ? "\n AND $where" : '');
			$sql .= "\n ORDER BY ordering";
			$sql .= "\n LIMIT 1";
		}

		$this->_db->setQuery( $sql );
//echo 'A: ' . $this->_db->getQuery();


		$row = null;
		if ($this->_db->loadObject( $row )) {
			$query = "UPDATE $this->_tbl"
			. "\n SET ordering = " . (int) $row->ordering
			. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $this->$k )
			;
			$this->_db->setQuery( $query );

			if (!$this->_db->query()) {
				$err = $this->_db->getErrorMsg();
				die( $err );
			}
//echo 'B: ' . $this->_db->getQuery();

			$query = "UPDATE $this->_tbl"
			. "\n SET ordering = " . (int) $this->ordering
			. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $row->$k )
			;
			$this->_db->setQuery( $query );
//echo 'C: ' . $this->_db->getQuery();

			if (!$this->_db->query()) {
				$err = $this->_db->getErrorMsg();
				die( $err );
			}

			$this->ordering = $row->ordering;
		} else {
			$query = "UPDATE $this->_tbl"
			. "\n SET ordering = " . (int) $this->ordering
			. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $this->$k )
			;
			$this->_db->setQuery( $query );
//echo 'D: ' . $this->_db->getQuery();


			if (!$this->_db->query()) {
				$err = $this->_db->getErrorMsg();
				die( $err );
			}
		}
	}
	/**
	* Compacts the ordering sequence of the selected records
	* @param string Additional where query to limit ordering to a particular subset of records. This is expected to be a valid (and safe!) SQL expression
	*/
	function updateOrder( $where='' ) {
		$k = $this->_tbl_key;

		if (!array_key_exists( 'ordering', get_class_vars( strtolower(get_class( $this )) ) )) {
			$this->_error = "WARNING: ".strtolower(get_class( $this ))." does not support ordering.";
			return false;
		}

		if ($this->_tbl == "#__content_frontpage") {
			$order2 = ", content_id DESC";
		} else {
			$order2 = '';
		}

		$query = "SELECT $this->_tbl_key, ordering"
		. "\n FROM $this->_tbl"
		. ( $where ? "\n WHERE $where" : '' )
		. "\n ORDER BY ordering$order2 "
		;
		$this->_db->setQuery( $query );
		if (!($orders = $this->_db->loadObjectList())) {
			$this->_error = $this->_db->getErrorMsg();
			return false;
		}
		// first pass, compact the ordering numbers
		for ($i=0, $n=count( $orders ); $i < $n; $i++) {
			if ($orders[$i]->ordering >= 0) {
				$orders[$i]->ordering = $i+1;
			}
		}

		$shift = 0;
		$n=count( $orders );
		for ($i=0; $i < $n; $i++) {
			//echo "i=$i id=".$orders[$i]->$k." order=".$orders[$i]->ordering;
			if ($orders[$i]->$k == $this->$k) {
				// place 'this' record in the desired location
				$orders[$i]->ordering = min( $this->ordering, $n );
				$shift = 1;
			} else if ($orders[$i]->ordering >= $this->ordering && $this->ordering > 0) {
				$orders[$i]->ordering++;
			}
		}
	//echo '<pre>';print_r($orders);echo '</pre>';
		// compact once more until I can find a better algorithm
		for ($i=0, $n=count( $orders ); $i < $n; $i++) {
			if ($orders[$i]->ordering >= 0) {
				$orders[$i]->ordering = $i+1;
				$query = "UPDATE $this->_tbl"
				. "\n SET ordering = " . (int) $orders[$i]->ordering
				. "\n WHERE $k = " . $this->_db->Quote( $orders[$i]->$k )
				;
				$this->_db->setQuery( $query);
				$this->_db->query();
	//echo '<br />'.$this->_db->getQuery();
			}
		}

		// if we didn't reorder the current record, make it last
		if ($shift == 0) {
			$order = $n+1;
			$query = "UPDATE $this->_tbl"
			. "\n SET ordering = " . (int) $order
			. "\n WHERE $k = " . $this->_db->Quote( $this->$k )
			;
			$this->_db->setQuery( $query );
			$this->_db->query();
	//echo '<br />'.$this->_db->getQuery();
		}
		return true;
	}
	/**
	*	Generic check for whether dependancies exist for this object in the db schema
	*
	*	can be overloaded/supplemented by the child class
	*	@param string $msg Error message returned
	*	@param int Optional key index
	*	@param array Optional array to compiles standard joins: format [label=>'Label',name=>'table name',idfield=>'field',joinfield=>'field']. This is expected to hold only valid (and safe!) SQL expressions
	*	@return true|false
	*/
	function canDelete( $oid=null, $joins=null ) {
		$k = $this->_tbl_key;
		if ($oid) {
			$this->$k = $oid;
		}
		if (is_array( $joins )) {
			$select = $k;
			$join = '';
			foreach( $joins as $table ) {
				$tblName = $this->getEscaped( $table['name'] );
				$idField = $this->getEscaped( $table['idfield'] );
				$jnField = $this->getEscaped( $table['joinfield'] );
				$select .= ",\n COUNT(DISTINCT `$tblName`.`$idField`) AS `count_".substr($tblName, 3)."_$idField`";
				$join .= "\n LEFT JOIN `$tblName` ON `$tblName`.`$jnField` = `$this->_tbl`.`$k`";
			}

			$query = "SELECT $select"
			. "\n FROM `$this->_tbl`"
			. $join
			. "\n WHERE `$this->_tbl`.`$k` = ". (int) $this->$k
			. "\n GROUP BY `$this->_tbl`.`$k`"
			;
			$this->_db->setQuery( $query );

			$obj = null;
			if (!$this->_db->loadObject($obj)) {
				$this->_error = $this->_db->getErrorMsg();
				return false;
			}
			$msg = array();
			foreach( $joins as $table ) {
				$tblName = $this->getEscaped( $table['name'] );
				$idField = $this->getEscaped( $table['idfield'] );
				$k = 'count_'.substr($tblName, 3).'_'.$idField;
				if ($obj->$k) {
					$msg[] = $table['label'];
				}
			}

			if (count( $msg )) {
				$this->_error = "noDeleteRecord" . ": " . implode( ', ', $msg );
				return false;
			} else {
				return true;
			}
		}

		return true;
	}

	/**
	 * Delete record
	 *
	 * @param int $oid override id
	 * @param boolean $deleteRelations
	 * @return boolean
	 */
	function delete($deleteRelations=false) {
		//if (!$this->canDelete( $msg )) {
		//	return $msg;
		//}

		$k = $this->_tbl_key;
		/*if ($oid) {
			$this->$k = intval( $oid );
		}*/

		if($this->_permissions > 1){
			if(count($this->_foreignObjects)){
				foreach($this->_foreignObjects as $fk => $dep){
					if(isset($this->$dep) && is_array($this->$dep) && count($this->$dep)){
						foreach($this->$dep as $rec){
							if(is_subclass_of($rec,"MhtmlModel")){
								$rec->delete($deleteRelations);
							}else{
								MhtmlDebug::log("Object not subclass of MhtmlModel",$rec);
							}
						}
					}elseif(isset($this->$dep) && !is_array($this->$dep) && is_subclass_of($this->$dep,"MhtmlModel")){
						if($this->$fk==$this->$dep->id){
							$this->$dep->delete($deleteRelations);
						}else{
							throw new GeneralException("The foreign key value has changed since loading of the dependancy \$this->$dep, unable to delete.");
						}
					}else{
						MhtmlDebug::log("Unknown foreign object",$this->$dep);
					}
				}
			}

			$query = "DELETE FROM $this->_tbl"
			. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $this->$k )
			;
			$this->_db->setQuery( $query );

			if ($this->_db->query()) {
				MhtmlDebug::getInstance()->trace("Deleting $this->_tbl($this->id) = Success");
				return true;
			} else {
				MhtmlDebug::getInstance()->trace("Deleting $this->_tbl($this->id) = Failed (".$this->_db->getErrorMsg().")");
				$this->_error = $this->_db->getErrorMsg();
				return false;
			}
		}else{
			MhtmlDebug::getInstance()->trace("Deleting $this->_tbl($this->id) = Failed (Insufficient Permissions)");
			$this->_error = "Delete failed - Insufficient Permissions";
			return false;
		}
	}

	/**
	 * Checks out an object
	 * @param int User id
	 * @param int Object id
	 */
	function checkout( $user_id, $oid=null ) {
		if (!array_key_exists( 'checked_out', get_class_vars( strtolower(get_class( $this )) ) )) {
			$this->_error = "WARNING: ".strtolower(get_class( $this ))." does not support checkouts.";
			return false;
		}
		$k = $this->_tbl_key;
		if ($oid !== null) {
			$this->$k = $oid;
		}

		$time = date( 'Y-m-d H:i:s' );
		if (intval( $user_id )) {
			$user_id = intval( $user_id );
			// new way of storing editor, by id
			$query = "UPDATE $this->_tbl"
			. "\n SET checked_out = $user_id, checked_out_time = " . $this->_db->Quote( $time )
			. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $this->$k )
			;
			$this->_db->setQuery( $query );

            $this->checked_out = $user_id;
            $this->checked_out_time = $time;
		} else {
			$user_id = $this->_db->Quote( $user_id );
			// old way of storing editor, by name
			$query = "UPDATE $this->_tbl"
			. "\n SET checked_out = 1, checked_out_time = " . $this->_db->Quote( $time ) . ", editor = $user_id"
			. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $this->$k )
			;
			$this->_db->setQuery( $query );

			$this->checked_out = 1;
			$this->checked_out_time = $time;
			$this->checked_out_editor = $user_id;
		}

		return $this->_db->query();
	}

	/**
	 * Checks in an object
	 * @param int Object id
	 */
	function checkin( $oid=null ) {
		if (!array_key_exists( 'checked_out', get_class_vars( strtolower(get_class( $this )) ) )) {
			return true;
			//$this->_error = "WARNING: ".strtolower(get_class( $this ))." does not support checkin.";
			//return false;
		}

		$k 			= $this->_tbl_key;
		$nullDate 	= $this->_db->getNullDate();

		if ($oid !== null) {
			$this->$k = intval( $oid );
		}
		if ($this->$k == NULL) {
			return false;
		}

		$query = "UPDATE $this->_tbl"
		. "\n SET checked_out = 0, checked_out_time = " . $this->_db->Quote( $nullDate )
		. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $this->$k )
		;
		$this->_db->setQuery( $query );

		$this->checked_out = 0;
		$this->checked_out_time = '';

		return $this->_db->query();
	}

	/**
	 * Increments the hit counter for an object
	 * @param int Object id
	 */
	function hit( $oid=null ) {
		global $mosConfig_enable_log_items;

		$k = $this->_tbl_key;
		if ($oid !== null) {
			$this->$k = intval( $oid );
		}

		$query = "UPDATE $this->_tbl"
		. "\n SET hits = ( hits + 1 )"
		. "\n WHERE $this->_tbl_key = " . $this->_db->Quote( $this->id )
		;
		$this->_db->setQuery( $query );
		$this->_db->query();

		if (@$mosConfig_enable_log_items) {
			$now = date( 'Y-m-d' );
			$query = "SELECT hits"
			. "\n FROM #__core_log_items"
			. "\n WHERE time_stamp = " . $this->_db->Quote( $now )
			. "\n AND item_table = " . $this->_db->Quote( $this->_tbl )
			. "\n AND item_id = " . $this->_db->Quote( $this->$k )
			;
			$this->_db->setQuery( $query );
			$hits = intval( $this->_db->loadResult() );
			if ($hits) {
				$query = "UPDATE #__core_log_items"
				. "\n SET hits = ( hits + 1 )"
				. "\n WHERE time_stamp = " . $this->_db->Quote( $now )
				. "\n AND item_table = " . $this->_db->Quote( $this->_tbl )
				. "\n AND item_id = " . $this->_db->Quote( $this->$k )
				;
				$this->_db->setQuery( $query );
				$this->_db->query();
			} else {
				$query = "INSERT INTO #__core_log_items"
				. "\n VALUES ( " . $this->_db->Quote( $now ) . ", " . $this->_db->Quote( $this->_tbl ) . ", " . $this->_db->Quote( $this->$k ) . ", 1 )"
				;
				$this->_db->setQuery( $query );
				$this->_db->query();
			}
		}
	}

	/**
	 * Tests if item is checked out
	 * @param int A user id
	 * @return boolean
	 */
	function isCheckedOut( $user_id=0 ) {
		if ($user_id) {
			return ($this->checked_out && $this->checked_out != $user_id);
		} else {
			return $this->checked_out;
		}
	}

	/**
	* Generic save function
	* @param array Source array for binding to class vars
	* @param string ignore
	* @param string Filter for the order updating. This is expected to be a valid (and safe!) SQL expression
	* @returns TRUE if completely successful, FALSE if partially or not succesful
	* NOTE: Filter will be deprecated in verion 1.1
	*/
	function save( $source=array(), $ignore='id', $updateNulls=false, $order_filter='' ) {
		if (!$this->bind( $source,$ignore )) {
			return false;
		}
		if (!$this->check()) {
			return false;
		}
		if (!$this->store($updateNulls)) {
			return false;
		}
		if (!$this->checkin()) {
			return false;
		}

		if ($order_filter) {
			$filter_value = $this->$order_filter;
			$this->updateOrder( $order_filter ? "`$order_filter` = " . $this->_db->Quote( $filter_value ) : '' );
		}
		$this->_error = '';
		return true;
	}

	/**
	 * @deprecated As of 1.0.3, replaced by publish
	 */
	function publish_array( $cid=null, $publish=1, $user_id=0 ) {
		$this->publish( $cid, $publish, $user_id );
	}

	/**
	 * Generic Publish/Unpublish function
	 * @param	array	An array of id numbers
	 * @param	integer	0 if unpublishing, 1 if publishing
	 * @param	integer	The id of the user performnig the operation
	 * @since	1.0.4
	 */
	function publish( $cid=null, $publish=1, $user_id=0 ) {
		mosArrayToInts( $cid, array() );
		$user_id	= (int) $user_id;
		$publish	= (int) $publish;
		$k			= $this->_tbl_key;

		if (count( $cid ) < 1) {
			$this->_error = "No items selected.";
			return false;
		}

		$cids = $this->_tbl_key . '=' . implode( ' OR ' . $this->_tbl_key . '=', $cid );

		$query = "UPDATE $this->_tbl"
		. "\n SET published = " . (int) $publish
		. "\n WHERE ($cids)"
		. "\n AND (checked_out = 0 OR checked_out = " . (int) $user_id . ")"
		;
		$this->_db->setQuery( $query );
		if (!$this->_db->query()) {
			$this->_error = $this->_db->getErrorMsg();
			return false;
		}

		if (count( $cid ) == 1) {
			$this->checkin( $cid[0] );
		}
		$this->_error = '';
		return true;
	}

	/**
	* Export item list to xml
	* @param boolean Map foreign keys to text values
	*/
	function toXML( $mapKeysToText=false ) {
		$xml = '<record table="' . $this->_tbl . '"';

		if ($mapKeysToText) {
			$xml .= ' mapkeystotext="true"';
		}
		$xml .= '>';
		foreach (get_object_vars( $this ) as $k => $v) {
			if (is_array($v) or is_object($v) or $v === NULL) {
				continue;
			}
			if ($k[0] == '_') { // internal field
				continue;
			}
			$xml .= '<' . $k . '><![CDATA[' . $v . ']]></' . $k . '>';
		}
		$xml .= '</record>';

		return $xml;
	}
}

?>